package com.weixiao.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.weixiao.domain.Goods;
import com.weixiao.domain.DingDan;
import com.weixiao.mapper.GoodsMapper;
import com.weixiao.service.GoodsService;
import com.weixiao.service.DingDanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
    @Autowired
    private DingDanService dingDanService;
    @Autowired
    private StringRedisTemplate redisTemplate;

    //查询参与秒杀的商品
    @Override
    public List<Goods> selectSeckillGoods() {
        QueryWrapper<Goods> wrapper = new QueryWrapper<>();
        wrapper.eq("spike", 1).eq("status", 1);
        List<Goods> goods = baseMapper.selectList(wrapper);
        return goods;
    }

    /**
     * 先查，再改，再更新，就会有并发安全问题
     * 使用分布式锁解决
     * mysql：
     * 利用行锁机制来控制分布式锁问题
     * update goods set total_stock = total_stock-1 where goods_id=1 and total_stock-1>0
     * <p>
     * 利用版本号，加一个version字段
     * update goods set total_stock = 1 ,version=version+1 where goods_id=1 and version=#{version}
     * redis(万能，并发性能都好)：setnx(set if not exist 如果这个key不存在，则设置成功，返回true)
     */
    //添加事务
    @Transactional(rollbackFor = RuntimeException.class) //发生异常回滚事务
    @Override
    public void realDoseckill(Integer userId, Integer goodsId) {
        /**
         * 真正处理秒杀业务的代码：查数据库，减库存，写订单
         * 要考虑安全问题，比如订单写失败了，前面操作全部作废，此时需要使用事务
         * 多个线程，同时查此数据库，会造成线程安全问题，此时要对数据库加锁
         */

        //写一个自旋锁，将线程放进一个循环中，抢不到就再来重新抢，多次抢不到再出循环返回
        //设置次数
        int chance=0;
        while (chance<5){
            //使用goodsId作为设置一个redis分布式锁，只有当锁没有被锁住时才能进行业务代码执行
            //锁需要设置一个过期时间，防止卡死现象
            Boolean flag = redisTemplate.opsForValue().setIfAbsent("goodslock:" + goodsId, "",120, TimeUnit.SECONDS);
            if (flag) {
                try {
                    //获取商品并查询库存
                    Goods goods = baseMapper.selectById(userId);
                    int count = goods.getTotalStocks() - 1;
                    System.out.println(count);
                    //库存不够直接返回
                    if (count < 0) {
                        System.out.println("卖完了！");
                        return;
                    }
                    System.out.println("执行成功");
                    //库存够就修改数据库，减库存
                    goods.setGoodsId(goodsId);
                    goods.setTotalStocks(count);
                    goods.setUpdateTime(new Date());
                    baseMapper.updateById(goods);
                    int i = baseMapper.updateById(goods);
                    System.out.println("执行成功");
                    //修改成功，写订单表
                    if (i > 0) {
                        //写订单表
                        writeOrder(userId, goodsId);
                    }
                } finally {
                    //业务执行完一定要将锁删除，给其他的线程执行
                    redisTemplate.delete("goodslock:" + goodsId);
                }
            } else {
                //如果没有抢到，就等一会，再去抢，连续抢不到只能怪运气不好
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //将时间加一点，连续抢不到，时间超过就只能出循环，自旋结束
                chance++;
            }
        }
    }

    //写订单表
    private void writeOrder(Integer userId, Integer goodsId) {
        System.out.println(userId + "  " + goodsId);
        //创建订单并添加进数据库
        DingDan dingDan = new DingDan();
        dingDan.setGoodId(goodsId);
        dingDan.setUserId(userId);
        dingDan.setCreateTime(new Date());
        System.out.println(dingDan);
        boolean i = dingDanService.save(dingDan);
        if (!i) {
            throw new RuntimeException("写订单失败");
        }
    }
}